{ buildEnv
, emacs30
, emacs29-macport
, fetchurl
, lib
, lzip
, makeWrapper
, runCommand
, runCommandLocal
, stdenvNoCC
, writeText
, pkgs
, version ? builtins.readFile ./VERSION
  # list of strings that map to the <name>.cfg.el files or "all".
, extraConfigs ? [ ]
}:

let

  emacsPackages = emacs30.pkgs.overrideScope' (eFinal: ePrev: {
    # https://bitbucket.org/mituharu/emacs-mac/src/master/README-mac
    emacs = if stdenvNoCC.hostPlatform.isDarwin then emacs29-macport else emacs30;
  });

  emacsCoreCfgPackages = {
    # TODO: add some test to ensure this is in sync.

    default = epkgs: (with epkgs.melpaPackages; [
      solarized-theme
      smart-mode-line
      use-package
      bind-key
      auto-compile
    ]) ++ (with epkgs.elpaPackages; [
      delight
    ]);

    core = epkgs: with epkgs.melpaPackages; [
      yasnippet
      yasnippet-snippets
    ] ++ (if lib.versionOlder "31" epkgs.melpaPackages.flycheck.version
    then [
      epkgs.melpaPackages.flycheck
    ] else [
      epkgs.melpaPackages.flycheck
    ]);

    gud = epkgs: with epkgs.melpaPackages; [
      dap-mode
    ];

    parens = epkgs: with epkgs.melpaPackages; [
      smartparens
      rainbow-delimiters
    ];

    appearance = epkgs: [ ];

    tls = epkgs: [ ];

    autocomplete = epkgs: with epkgs.melpaPackages; [
      lsp-mode
      lsp-ui
      company
      company-quickhelp
      helm-company
    ];

    buffer = epkgs: (with epkgs.melpaPackages; [
      direnv
      drag-stuff
      editorconfig
      popwin
      smartscan
      sr-speedbar
      ws-butler
    ]) ++ (with epkgs.melpaPackages; [
      clean-aindent-mode
    ]) ++ (with epkgs.elpaPackages; [
      windresize
      undo-tree
    ]);

    helm = epkgs: with epkgs.melpaPackages;[
      helm
      helm-core
      helm-c-yasnippet
      helm-projectile
      helm-gtags # if global is installed
      helm-ag # if ag is installed
      helm-ls-git # if git is installed
      helm-ls-hg # if mercurial is installed
    ];

    vcs = epkgs: with epkgs.melpaPackages; [
      magit
      git-modes
      diff-hl
    ];

    project = epkgs: with epkgs.melpaPackages; [
      projectile
    ];

  };

  emacsCfgPackages = {

    nix = {
      epkgs = epkgs: with epkgs.melpaPackages; [
        # nix is always included
        nix-mode
        nix-buffer
        nixpkgs-fmt
      ];
      pkgs = [ pkgs.nixpkgs-fmt ];
    };

    cc = epkgs: [ ];

    latex = epkgs: [ ];

    org-mode = epkgs: with epkgs.melpaPackages; [
      org-tree-slide
      org-tree-slide-pauses
      # org-mode is always included
    ];

    rst = epkgs: [ ];

    golang = epkgs: with epkgs.melpaPackages; [
      go-eldoc
      go-guru
      go-impl
      go-mode
      go-rename
    ];

    clojure = epkgs: (with epkgs.melpaPackages; [
      clojure-mode
      kibit-helper
      cider
    ]);

    markdown = epkgs: with epkgs.melpaPackages; [
      markdown-mode
    ];

    lua = epkgs: with epkgs.melpaPackages; [
      company-lua # also pulls in lua-mode
    ];

    terraform = epkgs: with epkgs.melpaPackages; [
      terraform-mode
      company-terraform
    ];

    yaml = epkgs: with epkgs.melpaPackages; [
      ansible
      ansible-doc
      yaml-mode
    ];

    typescript = epkgs: (with epkgs.melpaPackages; [
      typescript-mode
    ]);

    javascript = epkgs: with epkgs.melpaPackages; [
      js2-mode
      json-mode
      js-comint
    ];

    python = epkgs: with epkgs.melpaPackages; [
      sphinx-doc
      virtualenvwrapper
    ];

    rust = epkgs: with epkgs.melpaPackages; [
      cargo
      racer
      rust-mode
    ];

    groovy = epkgs: [
      epkgs.melpaPackages.groovy-mode
      epkgs.melpaPackages.jenkinsfile-mode
    ];

    docker = epkgs: [
      epkgs.melpaPackages.dockerfile-mode
    ];

  };

  availableConfigs = lib.subtractLists
    (builtins.attrNames emacsCoreCfgPackages)
    (builtins.map
      (x: builtins.head (lib.splitString ".cfg.el" x))
      (builtins.attrNames (lib.filterAttrs
        (name: value: value == "regular")
        (builtins.readDir ./init.d))));


  extraConfigs' = with lib.lists; with lib.strings;
    assert
    let
      diffList =
        if extraConfigs == "all"
        then [ ]
        else subtractLists availableConfigs extraConfigs;
    in
    lib.asserts.assertMsg
      (
        diffList == [ ]
      )
      ''
        ERROR!
        Ivalid values in extraConfigs: ${concatStringsSep ", " diffList}
        Choose between: ${concatStringsSep ", " availableConfigs}
      '';
    if extraConfigs == "all"
    then availableConfigs
    else
      intersectLists (unique (extraConfigs ++ [ "nix" "org-mode" ])) availableConfigs;

  userModulesDef = writeText "user-modules.el"
    (
      ''
        ;;; user-modules.el --- Summary
        ;;; Commentary:
        ;;; generated by nix
        ;;; Code:
      '' +
      (lib.concatStrings (builtins.map (x: "(use-package ${x}.cfg)\n") extraConfigs')) + ''
        (provide 'user-modules)
        ;;; user-modules ends here
      ''
    );

  defaultEmacsConfig = stdenvNoCC.mkDerivation {

    pname = "emacs-config";
    version = version;

    src = ./.;

    doCheck = false;

    installPhase = ''
      mkdir -p $out/share/emacs/site-lisp
      cp ${userModulesDef} $out/share/emacs/site-lisp/user-modules.el
      cp default.el $out/share/emacs/site-lisp/
      cp -r init.d $out/share/emacs/site-lisp/
      cp -r vendor $out/share/emacs/site-lisp/
    '';

  };

  extraPaths = lib.flatten (builtins.map (x: x.pkgs) (builtins.filter (x: (x ? "pkgs")) (lib.attrVals extraConfigs' emacsCfgPackages))
  );

in

buildEnv {
  name = "emacs-dev-env-${version}";

  meta = with lib; {
    description = "Custom Emacs Development Environment";
    platforms = emacsPackages.emacs.meta.platforms;
  };

  buildInputs = [ makeWrapper ];

  paths = [

    (
      emacsPackages.withPackages (epkgs: [ defaultEmacsConfig ] ++ (
        map (fn: fn epkgs) (
          (builtins.attrValues emacsCoreCfgPackages)
            ++ (builtins.map (x: if (x ? "epkgs") then x.epkgs else x)
            (lib.attrVals extraConfigs' emacsCfgPackages))
        )
      ))
    )

  ] ++ extraPaths;

  postBuild = lib.optionalString (lib.length extraPaths > 0) ''
    for program in emacs emacs-${emacsPackages.emacs.version} emacsclient; do
        wrapProgram $out/bin/$program \
        --prefix PATH : ${lib.makeBinPath extraPaths}
    done
  '';

}
